feat(cli): global -C flag for working-directory switching#2031
feat(cli): global -C flag for working-directory switching#2031fengmk2 wants to merge 50 commits into
Conversation
✅ Deploy Preview for viteplus-preview canceled.
|
|
✅ Staging deployment successful! Preview: https://viteplus-staging.void.app/ |
Manual verification stepsSetupgit fetch origin && git checkout feat/cwd-flag
pnpm install && pnpm bootstrap-cli # installs this branch as the global vp
vp --help | grep -- '-C' # the new global flag is listed( Create a scratch monorepo with no local vite-plus install, so the branch's bundled CLI runs end to end: mkdir -p /tmp/vp-c-demo/apps/web /tmp/vp-c-demo/apps/admin /tmp/vp-c-demo/packages/ui/src && cd /tmp/vp-c-demo
echo '{ "name": "demo", "private": true, "workspaces": ["apps/*", "packages/*"] }' > package.json
for a in web admin; do
echo "{ \"name\": \"$a\", \"private\": true }" > apps/$a/package.json
echo "<!doctype html><h1>$a</h1>" > apps/$a/index.html
done
echo '{ "name": "ui", "private": true, "type": "module", "main": "src/index.ts" }' > packages/ui/package.json
echo 'export const ui = 1' > packages/ui/src/index.ts1.
|
How to use the Graphite Merge QueueAdd the label auto-merge to this PR to add it to the merge queue. You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has enabled the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. This stack of pull requests is managed by Graphite. Learn more about stacking. |
fefbe9d to
e6ed290
Compare
- defaultPackage is consulted only at the invocation root: a member package's own config never redirects (or fails) a command already running in that member; regression case pins a member config pointing at a missing directory - inline --env.NAME=value pack flags no longer consume the next token, so an explicit entry after them stays a positional - complete the pack value-flag table from pack-bin.ts: --root, -F/--filter, -c/--config; add cac kebab-case twins to the Vite table - mark the two mid-case expected-failure steps continue-on-failure per the harness's new shell-like && semantics
…rkspace In a non-workspace layout the member IS its own invocation root (standalone package), where honoring defaultPackage is by design; the finding is about workspace members, so the case moves to a fixture with a workspaces field and asserts the pack runs in place.
- rewrite is_bare to mirror cac/mri parsing: a non-flag token after any non-boolean flag is that flag's value (required and optional alike, so vp dev --host 0.0.0.0 and --open /foo elicit again), and only tokens no flag consumes are positionals; the per-tool required-value tables are replaced by smaller boolean tables derived from the shipped --help - pack workspace selectors (-W/--workspace, -F/--filter) define their own target set and always bypass elicitation - document in the RFC that implicit -C runs inside the already-chosen CLI (single-version workspace model); full re-delegation for defaultPackage targets with their own install/runtime pin is tracked in #2057
The test module imports items explicitly; a restack conflict resolution kept the tests but dropped the import, which only all-targets compilation catches (workspace clippy now runs clean with CI's exact invocation).
- pack workspace selectors also bypass elicitation in inline form (--filter=ui, --workspace=packages/ui) - honor the -- option terminator: a token after it is an explicit positional, and bare -- stays bare - global vpr rejects a bare -C with no value instead of delegating it as a run argument (mirrors the local wrapper fix) - local bin.ts catches process.chdir failures (e.g. permission denied on an existing directory) as a normal CLI error instead of an uncaught stack trace - render the spread-config build case through the PTY: non-TTY steps record raw bytes and the Windows byte stream of a vite build differs invisibly from the recorded macOS one
vp pack --root packages/lib at a workspace root was a valid targeted invocation before this PR; like -W/--workspace and -F/--filter it defines pack's own target, so it bypasses elicitation (spaced and inline forms) instead of hitting the picker or the non-TTY listing.
Match the documented convention in packages/prompts/src/milestone.ts: only the PTY snapshot harness sets the env, so real terminals and piped output never receive the OSC 8 marker bytes. The picker fixtures keep passing because the harness sets the gate for spawned processes.
fs.statSync can itself throw (EACCES on a parent, ELOOP) before the chdir try/catch; chdir reports every failure mode through one catchable path, with ENOENT/ENOTDIR keeping the directory-not-found message.
- one apply_chdir helper (join/clean/validate/chdir/PWD) replaces the four drifting copies in vite_global_cli: main's normal path, the completion peek, vpr, and the clap-path fallback in cli.rs — which previously skipped set_current_dir and the PWD sync, silently weakening the cd equivalence on orderings like a second -C - one classify() core (bare -> defaultPackage-at-root -> workspace root) now feeds both resolve_app_target and needs_elicitation, so the RFC's resolution order is written once and the two entry points cannot drift; the extracted defaultPackage value rides the enum, removing a duplicate config parse - bin/vpr is a grammar-free shim: bin.ts inserts 'run' itself via argv0 after its existing -C consumption, deleting the hand-synchronized second JS copy of the -C token grammar - pack runnable check stats src/index.ts before parsing the config (runs per workspace package); PWD in the direct-subcommand env is only overridden when elicitation retargeted (plain runs keep the caller's possibly-symlinked PWD verbatim); vpr parses -C once; identical FieldMapInner arms collapse via or-patterns; handler's -C predicate simplifies to starts_with
The helper was inserted between normalize_args's bullet-list doc comment and the function, merging the docs; clippy reads the helper's prose as unindented list continuations under --deny warnings.
The vp-config e2e broke: a single package with a settings-only pnpm-workspace.yaml (catalogs, minimumReleaseAge) is a workspace root, and the runnable-root picker row made the candidate list non-empty, so non-TTY CI got the target listing and exit 1 for a bare vp pack that packed fine before this branch. Deeper rule instead of the row: when the workspace root itself looks runnable for the command, classify() never elicits and the command runs in place, exactly the pre-elicitation behavior. The root is never a picker/listing row; elicitation only engages when the root is not a plausible target, which is what keeps the feature purely additive. New settings_only_workspace fixture pins the vp-config shape non-TTY; app_root_runnable_root now asserts in-place runs for both TTY modes.
The tty=false vite-build capture differs invisibly on Windows (same class as the round-9 unanalyzable_config_ignored fix); settings_only_workspace already covers the non-TTY in-place guarantee with Windows-stable tsdown output.
…3 rebase The rebase onto main (v0.2.3, vite-task 417b6aae) conflicted on the pin: 417b6aae ships vite_select and encoded_milestone but not the prompt param the picker needs (#510 is still open). ca55bb232aa8 is #510's current head (prompt param + main merged in), missing only two dependency-bump chores from 417b6aae that don't touch the crates vp consumes. Final pin moves to the merged SHA once #510 lands.
A bare app command walked find_workspace_root twice: once in classify to decide whether to elicit, once in execute_direct_subcommand to set up the session. classify now returns the root it found (Classification::RunInPlace) whenever the command runs in the unchanged cwd, and the caller reuses it. The hot path is a bare vp dev/build deep in a large monorepo: it now does one upward walk instead of two, and still never enumerates packages (that only happens at a root that actually elicits). The hint is handed back only for the run-in-place cases, where the effective cwd equals the original, so it always matches a fresh lookup; retargeted (-C, picker, defaultPackage) paths still resolve the target's own root.
Fast-forward over the previous ca55bb23 pin (contains it), so no divergence. The new head merged main, so it now carries main's v0.2.3 release commit (417b6aae, the two dep-bump chores my earlier pin was missing) plus ef49b4ac (fix: clarify task process wait errors, #515) and the v0.2.4 dep bump. The prompt param the picker needs is unchanged; snapshots are byte-stable. Final pin still moves to the merged SHA once #510 lands.
An interactive PTY case can wedge unbounded on the Namespace Linux runner (PTY spawn/teardown flakiness under the parallel harness, the Linux analog of the ConPTY blocking that keeps the Windows snapshot job on a GitHub-hosted runner). The harness's 50s per-step timeout does not cover TestTerminal::spawn, and the RwLock execution gate then stalls the whole suite. With no job timeout that burned the 6h default and never flushed the step log. A 10-min step bound (healthy runs finish in under 2 min) fails fast and preserves the partial log so the wedged case is identifiable.
vpr had no snapshot coverage: add vpr -C <dir> <task> and its cd-form equivalent (both print the target cwd, proving the global vpr shim and local bin/vpr consume -C) plus bare vpr -C (missing-value error) to cwd_flag. The dev/build/preview positional had only is_bare unit coverage: add vp build apps/web to app_root_listing, proving a positional forwards to Vite as [root] (output at apps/web/dist, cwd unchanged) and skips elicitation entirely (no Selected/Tip lines), unlike the -C forms.
vite-task #510 (configurable selector prompt) merged, so move the pin off the now-defunct PR-head commit (28310dc6) to the permanent main commit that merged it. Content-identical for the crates vp consumes (empty tree diff on vite_select/pty_terminal_test), so this is purely a stable-pin move: the picker's prompt param and snapshots are unchanged.
Two regressions surfaced in review, each reproduced by a new snapshot fixture: - defaultPackage declared through a TypeScript wrapper (satisfies UserConfig, as const) was ignored: extract_config_from_expr only stripped parentheses, so the wrapper made the config unanalyzable. Unwrap TS-only wrappers via get_inner_expression. Fixture default_package_ts_wrapper + extractor unit tests. - a workspace root that builds via vite.config.ts (build.lib/SSR, no root index.html) was filtered out of elicitation once a member existed, so non-interactive vp build listed members instead of building the root. The root is now runnable when it declares a build block, not only when it has index.html (a shared lint/fmt config still declares neither). Fixture config_only_root_build.
The recurring Linux snapshot hang survives #2102's spawn bound (a run that contained the bound still wedged for the full 10-min step timeout), so the wedge is at a different, still-unbounded point (or cumulative create-test timeouts around the ctrl-c case). Add snap_trace (gated on VP_SNAP_TRACE=1), writing START/phase/END lines straight to fd 2 so they survive libtest capture and reach the CI log even when the step is killed by its timeout. A step that logs START without END is the wedged one, and the phase markers (spawn dispatched / spawned / waiting for exit) localize within it. Enable it on the Linux PTY CI step; off by default everywhere else.
…eview
Three review follow-ups, each reproduced first:
- unwrap TS wrappers INSIDE defineConfig too: defineConfig({...} satisfies
UserConfig) and factory returns now analyze, not just the outer export.
(default_package_define_config_wrapper + extractor unit tests)
- the declared-build root signal is build-only: a lib/SSR root is a target
for vp build, but vp dev/preview still need index.html, so dev/preview at
a lib root elicits instead of serving the root. (dev_at_lib_root_elicits)
- an explicit -c/--config file is an explicit target: vp build -c <cfg>
forwards to Vite instead of eliciting. (explicit_config_forwards + is_bare
unit tests)
The last hang confirmed the trace works (the mac job logged clean START/END for every step) but GitHub discards a timed-out step's own log, so the wedged Linux job's trace was unrecoverable. Write the trace to VP_SNAP_TRACE_FILE instead and print it from a following if: always() step, which runs even after the PTY step is killed by its timeout. The next Linux wedge will show the START-without-END case in the Print PTY trace step. VP_SNAP_TRACE=1 still goes to stderr for local runs.
config_only_root_build and explicit_config_forwards captured a vite build with tty=false; a piped vite build's raw byte stream differs invisibly on Windows (the round-9 unanalyzable_config_ignored class), so both failed the Windows snapshot job. Render them through the PTY grid instead, which normalizes the bytes; the assertions (root builds in place, -c config forwards) are unchanged. dev_at_lib_root_elicits stays tty=false: it captures the plain-text elicitation error, which is Windows-stable.
- vpr: restore dynamic import so it runs after enableCompileCache (a static import is hoisted, defeating the compile cache for the CLI bundle) - static config: unwrap TS-only wrappers (as const, satisfies) on property values so defaultPackage: './x' as const stays a static string literal; add extractor unit tests and a value_wrapper snapshot fixture - global -C: deliver PWD to delegated children via the spawn env instead of mutating our own PWD, which is unsound once the Tokio runtime has threads
The new expr_to_json TS-wrapper assertions exceeded the line width; cargo fmt --check gates CI Lint.
Diagnostic scaffolding (snap_trace, VP_SNAP_TRACE_FILE, the Print PTY trace step, and the 10-min step bound) for the Namespace-Linux PTY wedge. The hang did not reproduce across 40 retry attempts, so this drops the investigation code from the feature PR; restore ci.yml and the harness to their base state.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b0ae86a517
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if !target.as_path().is_dir() { | ||
| return Err(format!("directory not found: {dir}")); | ||
| } | ||
| if let Err(e) = std::env::set_current_dir(target.as_path()) { |
There was a problem hiding this comment.
Preserve PWD for -C package-manager children
For vp -C app install / vp -C app pm ..., this only changes the process cwd; the package-manager path goes through vite_pm_cli to vite_command::run_command, which inherits the existing environment and only overlays command-specific envs, so PWD still points at the original directory. Any npm/pnpm lifecycle script or child command that reads process.env.PWD observes the wrong project, so -C is not equivalent to cd app && vp ... for these commands. Please set PWD on spawned non-JS children when a -C target is applied.
Useful? React with 👍 / 👎.

Bare
vp dev/build/preview/packat a monorepo root used to silently run against the root (a 404 dev server, a build of the wrong thing). Now they resolve a target.In a terminal, a fuzzy package picker (the same component
vp runuses):Typing filters the list. Enter runs the choice as an implicit
-Cand prints how to skip the prompt next time:Non-interactive (CI, pipes), it lists the candidates and exits 1 instead of building the root:
The picker is built on a new global
-C <dir>flag:vp -C <dir> <cmd>is exactlycd <dir> && vp <cmd>for every command. Resolution order for a bare app command:-Cor a positional, thendefaultPackagein the root config (statically extracted, so it works at framework roots with no vite-plus install), then auto-select when exactly one package is runnable, then the picker in a TTY or the listing + exit 1 without one. A runnable root still runs in place, and positionals keep upstream semantics (Vite[root], tsdown entries).Implements
rfcs/cwd-flag.md. Tested on the PTY snapshot harness with real keystrokes in both the local and global vp flavors.Requires voidzero-dev/vite-task#510 (configurable selector prompt); the vite-task pin points at that PR and needs a re-bump to vite-task main after it merges.